/**
 * Copyright (c) 2014, 2019 The xterm.js authors. All rights reserved.
 * @license MIT
 *
 * Implements the attach method, that attaches the terminal to a WebSocket stream.
 */

import { Terminal, IDisposable, ITerminalAddon } from 'xterm';

interface IAttachOptions {
  bidirectional?: boolean;
  onError?: (err: Error) => void;
  onClose?: () => void;
}

export class AttachAddon implements ITerminalAddon {
  private _socket: WebSocket;
  private _bidirectional: boolean;
  private _disposables: IDisposable[] = [];
  private _onClose: () => void;
  private _onError: (err: Error) => void;

  constructor(socket: WebSocket, options?: IAttachOptions) {
    this._socket = socket;
    // always set binary type to arraybuffer, we do not handle blobs
    this._socket.binaryType = 'arraybuffer';
    this._bidirectional = options && options.bidirectional === false ? false : true;

    this._onClose = options ? options.onClose : undefined;
    this._onError = options ? options.onError : undefined;
  }

  public activate(terminal: Terminal): void {
    this._disposables.push(
      addSocketListener(this._socket, 'message', (ev) => {
        const data: ArrayBuffer | string = ev.data;
        terminal.write(typeof data === 'string' ? data : new Uint8Array(data));
      })
    );

    if (this._bidirectional) {
      this._disposables.push(terminal.onData((data) => this._sendData(data)));
    }

    this._disposables.push(
      addSocketListener(this._socket, 'close', (e) => {
        if (this._onError && e.code === 1011 && e.reason) {
          this._onError(new Error(e.reason));
        }

        this.dispose();
      })
    );
    this._disposables.push(addSocketListener(this._socket, 'error', () => this.dispose()));
  }

  public dispose() {
    this._disposables.forEach((d) => d.dispose());

    if (this._onClose) {
      this._onClose();
    }
  }

  private _sendData(data: string): void {
    // TODO: do something better than just swallowing
    // the data if the socket is not in a working condition
    if (this._socket.readyState !== 1) {
      return;
    }
    this._socket.send(data);
  }
}

function addSocketListener<K extends keyof WebSocketEventMap>(
  socket: WebSocket,
  type: K,
  handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any
): IDisposable {
  socket.addEventListener(type, handler);
  return {
    dispose: () => {
      if (!handler) {
        // Already disposed
        return;
      }
      socket.removeEventListener(type, handler);
    },
  };
}
